上一篇貼文講了PE的架構,這篇會從PE的頭部下刀
深入了解DOS Header、DOS Stub
並且寫一個小parser來讀取Rich Header的資料
--介紹--
為一段長度為64 bytes的區段,位在PE檔案的開頭,為了讓現在的執行檔可以向下相容MS-DOS而存在。如果沒有此片段,則無法在MS-DOS上執行。
我們可以從winnt.h
找到IMAGE_DOS_HEADER
的定義,藉此了解他的架構
MS-DOS的loader會根據此header來把執行檔寫入記憶體
以目前大部分的Windows系統來說,只會用到這個header裡面的兩個變數:
MZ
// 透過DOS Header找到NT Headers的位置
IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)fbuf; // fbuf == pointer to data read from file
IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((size_t)dosHeader + dosHeader->e_lfanew);
PE Bear的DOS Hdr pane
--介紹--
從Microsoft的官方文件我們可以知道,DOS Stub是MS-DOS系統的執行檔,只要是合法的MS-DOS程式都可以被塞到這個區塊。而在沒有特別指定的情況下,功能為印出"This program cannot be run in MS-DOS mode."這串錯誤訊息。
--小實驗--
實際用DOSBox模擬看看在DOS系統底下執行PE執行檔
操作步驟:
test
notepad
執行檔複製到test
資料夾裡面MOUNT C C:\test
notepad
執行結果應證了文件所述,但我還是很好奇他到底是怎麼運行的,於是決定來拆解他
--逆一下--
把DOS Stub分離出來會比較好做分析,底下是分離的步驟
我們先用PE Bear複製該程式片段
在讀完檔案後,點到DOS Stub區域,選取到Rich Header的offset之前,並把padding的部分刪掉
選取之後按右鍵複製並貼到HxD,把檔案存成dos_stub.exe
分離好之後就可以開始分析的環節了
我決定用radare2來操作,因為他很酷
在wsl環境下載完之後,就可以輸入r2 dos_stub.exe
來開啟檔案。用pd
印出組合語之後,會發現顯示的指令有點怪怪的,這是因為當時的處理器為16位元,與目前大家常用的64位元處理器指令集不一樣
這邊我們可以透過e asm.bits=16
修正成正確的指令。
好多了,但還是有地方怪怪的,程式不小心把字串解讀成指令了。
修正的步驟也很簡單,只要按V
進入hex mode,並按c
再按住shift
透過hjkl或上下左右鍵進行連續選取,把字串的部分全部框起來。(藉由剛剛的小實驗,我們可以知道字串的部分是從This開始一路到最後面)
按d
顯示選項,可以看到有非常多的設定方式,這邊我們想要把選取的片段設成data,所以再按一次d
設定完成後按q
返回,並再試一次pd
印出組合語
根據之前的逆向經驗,簡單把程式分成三個部分
第一段是把data segment的值設定成code segment的值,可以看成ds = cs。
0000:0000 0e push cs
0000:0000 1f pop ds
第二段與第三段都出現int 0x21
的instruction
0000:0002 ba0e00 mov dx, 0xe
0000:0005 b409 mov ah, 9
0000:0007 cd21 int 0x21
0000:0009 b8014c mov ax, 0x4c01
0000:000c cd21 int 0x21
根據DOS API的wiki,可以查到int 0x21是interrupt vector
透過設定ah的值來決定系統要採取什麼行動,下圖是ah數值對應到的指令表
所以我們可以知道
第二段的功能就是把在0000:000e上面的資料("This program ...")印出來
第三段的功能就是以return value = 1的狀態下終止程序
若想再深入了解DOS Stub,並嘗試更改Stub的行為,可以看這裡
--介紹--
介於DOS Stub與NT Headers之間的片段,用Visual Studio工具集把程式編譯成執行檔而生成的資訊,不是PE檔案格式的一部分。2018年的Olympic Destroyer病毒就是透過更改此片段來使分析過程變得困難。詳細資訊可以看這篇解析
Rich Header是由一整個chunk的加密訊息、一個singature以及一個4 bytes的XOR key所組成的。被加密的chunk由一個signature以及三個DWORD(4 bytes)大小的0作為開頭,緊接著的是一連串的DWORD pair,每個DWORD pair可以解出三組數據,分別代表使用的編譯工具(Product ID)、build ID、use count
--小實驗--
寫一個小parser當作練習,操作步驟:
// 透過DOS Header找到NT Headers的位置
IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)fbuf;
IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((size_t)dosHeader + dosHeader->e_lfanew);
// 假設DOS Header跟DOS Stub的長度都固定 => Rich header從0x80開始
// 檢查Rich Header是否存在
if (dosHeader->e_lfanew == 0x80) {
printf("Rich Header does not exist\n");
return;
}
// 找到Rich signature,得到key的值
memcpy(richbuf, fbuf + 0x80, richLen);
tok = strstr(richbuf, "Rich");
richOffset = tok - richbuf;
if (tok == NULL) {
printf("Broken binary\n");
return;
}
memcpy(key, tok + 4, 4);
// 用key把chunk分別解出來
for (int i = 0; i < richOffset; i++) {
output[i % 8] = richbuf[i] ^ key[i % 4];
if (i % 8 == 7) printHex(output);
}
// 處理大小端,並把資訊印出來
void printHex(char *output)
{
unsigned char *ptr;
unsigned char ldword[4], udword[4];
char uformatted[16], lformatted[16];
for (int i = 0; i < 4; i++) {
ptr = output + i;
ldword[4-i-1] = *ptr;
udword[4-i-1] = *(ptr + 4);
}
for (int i = 0; i < 4; i++) {
sprintf(lformatted + i * 2, "%02x", ldword[i]);
sprintf(uformatted + i * 2, "%02x", udword[i]);
}
printf("%s %s - ", lformatted, uformatted);
if (strcmp(lformatted, "536e6144") == 0) {
printf("%-8s", "DanS");
} else {
printf("%d.%d.%d",
ldword[2] * 256 + ldword[3],
ldword[0] * 256 + ldword[1],
udword[2] * 256 + udword[3]);
}
printf("\n");
return;
}
編譯後去parse看看notepad.exe的rich header
用PE Bear也可以得到一樣的結果
richParser.c : source
(successfully built on Windows11 with gcc 11.2.0)
我們提到了DOS Header的架構,並了解到這個Header對當前的Windows系統來說不是特別重要。
看完DOS Stub的官方文件之後我們決定開模擬器實際執行一次,接著用radare2把它拆了。
最後我們寫了一個parser去把Rich Header這個神秘區段的資料撈出來,並用PE Bear驗證。
下一篇文章,我們將會開始處理與現在Windows系統比較相關的NT Headers
0xRick's dive into the PE file format
How to end assembly correctly?
What's the purpose of PUSH CS / POP DS before a REP MOVSW?
winnt.h
radare2 meets com101
How To Run Dos Program in Windows 10 64 Bit using DOSBox Tutorial
Exploring the MS-DOS Stub